import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.awt.geom.Point2D;


/**
 * Stores all locations for a given country
 * Enables searches for alternative locations
 * 
 * @author tomalteman
 *
 */
public class locList {
	
	private HashMap<String,location> list;
	public int total;
	private HashMap<Integer,HashMap<Integer,ArrayList<location>>> grid;
	private ArrayList<location> found;
	private int debug=0;
	
	//Internal obj
	private double ycoord;
	private double xcoord;
	
	public locList(){
		list = new HashMap<String,location>();
		total=0;
		ycoord = 0;
		xcoord = 0;
		grid = new HashMap<Integer,HashMap<Integer,ArrayList<location>>>();
		found = new ArrayList<location>();
	}
	
	/*
	 * Loads UK loc list straight away
	 */
	public locList(int i) {
		list = new HashMap<String,location>();
		total=0;
		ycoord = 0;
		xcoord = 0;
		grid = new HashMap<Integer,HashMap<Integer,ArrayList<location>>>();
		found = new ArrayList<location>();
		load("locations/uk.txt");
	}
	
	/**
	 * Adds a location to hashmap
	 * @param loc
	 */
	public void add(location loc)
	{
		list.put(loc.name,loc);
	}
	
	///////////////////////////////
	///// LOADING FUNCTIONS
	///////////////////////////////
	/**
	 * Load locations from file
	 * @param s
	 */
	public void load(String s)
	{
		//Read in tab seperated text file
		ArrayList<String> input = TFile.read(new File(s));
		String[] line;
		String tab = Character.toString('\t');
		location loc;
		
		for(int i=0;i<input.size();i++)
		{
			loc = new location();
			line = (input.get(i)).split(tab);
			
			if(debug==1) System.out.println("Parsing line: "+input.get(i));
			
			//Should have 4 values
			if(line.length > 3)
			{
				
				//Load in name
				loc.name = line[0].trim().toLowerCase();
				if(debug==1) System.out.println("Name is: "+loc.name);
				
				//Get latitude and long - do not bother continuing if they arent there
				if(parseGPS(line[1],0) && parseGPS(line[2],1))
				{
					//Add coords
					loc.latitude = ycoord;
					loc.longitude = xcoord;
					
					//Get variant
					if(debug==1) System.out.println("Variant is: "+line[3]);
					loc.variant = splitAndTrim(line[3],",");
					
					//If there is a metropolitan area
					if(line.length==5)
					{
						//Get Met area
						if(debug==1) System.out.println("Met Area is: "+line[4]);
						loc.metarea = splitAndTrim(line[4],"-");
					}
					
					//Add location to locList
					add(loc);
					
				}
			}
		}
		loadGrid();
	}
	
	/**
	 * Loads locations into hash gird
	 */
	private void loadGrid()
	{
		location loc;
		int x,y;
		HashMap<Integer,ArrayList<location>> hm;
		ArrayList<location> l;
		
		Iterator iter = list.entrySet().iterator();
	    while (iter.hasNext()) {
	        Map.Entry<String,location> val = (Map.Entry<String,location>)iter.next();
	        loc = val.getValue();
	        
	        x = (int) (loc.longitude*10.0);
	        if((x % 2)!=0) 
	        	{
	        		if(x<0) x= x+1;
	        		else x = x-1;
	        	}
	        y= (int) (loc.latitude *10.0);
	        if((y % 2)!=0) 
	        	{
	        		if(y<0) y=y+1;
	        		else y= y-1;
	        	}
	        
	        //System.out.println(loc.longitude + " : "+x);
	        
	        //Get Y coord list
	        if(grid.containsKey(x))
	        {
	        	hm = grid.get(x);
	        }
	        else
	        {
	        	hm = new HashMap<Integer,ArrayList<location>>();
	        }
	        
	        //Put location in Y coord list
	        if(hm.containsKey(y))
	        {
	        	l = hm.get(y);
	        }
	        else
	        {
	        	l = new ArrayList<location>();
	        }
	        
	        l.add(loc);
	        
	        /*System.out.print("For "+x+":"+y);
	        for(location e : l)
	        {
	        	System.out.print(" "+e.name);
	        }
	        System.out.println();*/
	        
	        hm.put(y, l);
	        
	        grid.put(x, hm);
	    }
	}
	
	/**
	 * Given GPS string from file, convert into grid coord value
	 * xy is 0 for latitude / 1 for longitude
	 * @param s
	 * @return
	 */
	private boolean parseGPS(String s, int xy)
	{
		//Prep
		s = s.trim();
		double mult = 1;
		
		try {
		//Split on -
		String[] split = s.split("-");
		
		
		
		//If after * is S or W then coord becomes negative
		if(split[1].toLowerCase().equals("s") || split[1].toLowerCase().equals("w")) mult = -1;
		
		//Get number and multiply by mult to get coord grid value
		if(xy == 0) ycoord = Double.parseDouble(split[0]) * mult;
		else xcoord = Double.parseDouble(split[0]) * mult;
		
		if(debug==1)
			{
				if(xy==0) System.out.println("Parse coord " + s + " becomes "+ycoord);
				else System.out.println("Parse coord " + s + " becomes "+xcoord);
			}
		
		} catch (Exception e) {
			if(debug==1) System.err.println("CANNOT PARSE GPS COORD: "+s + " DUE TO "+e);
			return false;
		}
		
		return true;
		
	}
	
	private ArrayList<String> splitAndTrim(String s, String regex)
	{
		ArrayList<String> places = new ArrayList<String>();
		
		String[] split = s.trim().split(regex);
		String w;
		
		for(int i=0;i<split.length;i++)
		{
			w = split[i].trim();
			
			if((w.length()>1) && isValid(w))
				{
					places.add(w.toLowerCase());
					if(debug==1) System.out.println("Add to list "+w);
				}
			
		}
		
		return places;
	}
	
	private int roundVal(double l)
	{
		int y= (int) (l *10.0);
        if((y % 2)!=0) 
        	{
        		if(y<0) y=y+1;
        		else y= y-1;
        	}
        
        return y;
	}
	
	/**
	 * Checks to see if none of the invalidating chars are present
	 * @param s
	 * @return
	 */
	private boolean isValid(String s)
	{
		if(s.contains("(")) return false;
		
		if(s.contains("/")) return false;
		
		return true;
	}
	
	///////////////////////////////
	///// FINDING FUNCTIONS
	///////////////////////////////
	
	public ArrayList<location> getNeighbours(String s, double radius)
	{
		ArrayList<location> locs = new ArrayList<location>();
		ArrayList<location> search = new ArrayList<location>();
		location base,loc;
		//Find original location info
		s = s.toLowerCase();
		
		//If it can't be found then no search possible
		if((base = list.get(s)) == null) return search;
		
		//Add base to found
		found.add(base);
		
		//Get grid square for base
		//locs = getGrid(base.longitude,base.latitude);
		
		//if(locs.size() > 0) return locs;
		//print(locs,"LOCS ARE");
		//Extend search to adjacent squares
		locs = gridArea(base,radius);
		int j=0;
		
		//Calcualte distance and add them accordingly
		for(int i=0;i<locs.size();i++)
		{
			loc = locs.get(i);
			//System.out.println(loc.name);
			if((loc.distance = Point2D.distance(loc.latitude, loc.longitude, base.latitude, base.longitude)) <= radius)
			{
				//Add in order to list
				for(j=0;j<search.size();j++)
				{
					if(search.get(j).distance>loc.distance) break;
				}
				search.add(j,loc);
				found.add(loc);
			}
		}
		
		return search;
	}
	
	private ArrayList<location> gridArea(location base, double radius)
	{
		ArrayList<location> locs = new ArrayList<location>();
		
		int x,y;
		
		int xmin = roundVal(base.longitude - radius);
		int xmax = roundVal(base.longitude + radius);
		int ymin = roundVal(base.latitude - radius);
		int ymax = roundVal(base.latitude + radius);
		
		y=ymin;
		
		while (y<=ymax)
		{
			x = xmin;
			while (x <=xmax)
			{
				addLocs(locs,getGrid(x,y));
				x = x + 2;
			}
			y = y + 2;
		}
		
		return locs;
	}
	
	private ArrayList<location> isFound(ArrayList<location> loc)
	{
		location l;
		ArrayList<location> nloc = new ArrayList<location>();
		for(int i=0;i<loc.size();i++)
		{
			l = loc.get(i);
			if(!found.contains(l)) nloc.add(l);
		}
		
		return nloc;
	}
	
	private void addLocs(ArrayList<location> addto, ArrayList<location> loc)
	{
		location l;
		for(int i=0;i<loc.size();i++)
		{
			addto.add(loc.get(i));
		}
	}
	
	/**
	 *  Get a grid space
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	private ArrayList<location> getGrid(double longitude, double latitude)
	{
		int x = (int) (longitude*10.0);
        if((x % 2)!=0) 
        	{
        		if(x<0) x= x+1;
        		else x = x-1;
        	}
        int y= (int) (latitude *10.0);
        if((y % 2)!=0) 
        	{
        		if(y<0) y=y+1;
        		else y= y-1;
        	}
		
		ArrayList<location> loc = new ArrayList<location>();
		
		if(!grid.containsKey(x)) return loc;
		
		HashMap<Integer,ArrayList<location>> hm = grid.get(x);
		
		
		//print(isFound(hm.get(y)),"Get GRID");
		
		if(!hm.containsKey(y)) return loc;
		
		return isFound(hm.get(y));
	}
	
	private void print(ArrayList<location> loc,String s)
	{
		System.out.println("*************");
		System.out.println(s);
		System.out.println("********");
		for(int i=0;i<loc.size();i++)
		{
			System.out.println(loc.get(i).name);
		}
		System.out.println("*************");
	}
	
	private ArrayList<location> getGrid(int x, int y)
	{
		ArrayList<location> loc = new ArrayList<location>();
		
		if(!grid.containsKey(x)) return loc;
		
		HashMap<Integer,ArrayList<location>> hm = grid.get(x);
		
		if(!hm.containsKey(y)) return loc;
		
		return isFound(hm.get(y));
	}
	
	public ArrayList<location> getN(String s, double radius)
	{
		ArrayList<location> search = new ArrayList<location>();
		location base,loc;
		//Find original location info
		s = s.toLowerCase();
		
		//If it can't be found then no search possible
		if((base = list.get(s)) == null) return search;
		
		//Remove base from list
		list.remove(s);
		
		int j=0;
		
		//Iterate through list and check against locations
		Iterator iter = list.entrySet().iterator();
	    while (iter.hasNext()) {
	        Map.Entry<String,location> val = (Map.Entry<String,location>)iter.next();
	        loc = val.getValue();
	        
	        //Check if bounded
	       if(isBounded(loc.latitude,base.latitude,radius) && isBounded(loc.longitude,base.longitude,radius))
	       {
	    	   if((loc.distance = Point2D.distance(loc.latitude, loc.longitude, base.latitude, base.longitude)) <= radius)
				{
					//Add in order to list
					for(j=0;j<search.size();j++)
					{
						if(search.get(j).distance>loc.distance) break;
					}
					search.add(j,loc);
					found.add(loc);
				}
	       }
	    }
		
	    //Remove search list to avoid repeat searches
	    for(int i=0;i<search.size();i++) list.remove(search.get(i));
	    
	    //Add base back in
	    list.put(base.name,base);
	    
		return search;
	}
	
	/*
	 * Indicates whether string is a location
	 */
	public boolean contains(String s)
	{
		return list.containsKey(s.toLowerCase());
	}
	
	/**
	 * Check to see if coord is within radius of base
	 * @param coord
	 * @param base
	 * @param r
	 * @return
	 */
	private boolean isBounded(double coord,double base, double r)
	{
		if((coord>= (base-r)) && (coord<= (base+r))) return true;
		
		return false;
	}
	
	private void print()
	{
		location loc;
		
		Iterator iter = list.entrySet().iterator();
	    while (iter.hasNext()) {
	        Map.Entry<String,location> val = (Map.Entry<String,location>)iter.next();
	        loc = val.getValue();
	        System.out.println("LIST "+loc.name);
	    }     
	}
	
	private void toFile()
	{
		ArrayList<location> locs = getList();
		ArrayList<String> write = new ArrayList<String>();
		location loc;
		String line;
		String tab = Character.toString('\t');
		
		for(int i=0;i<locs.size();i++)
		{
			loc = locs.get(i);
			
			line = loc.name;
			
			if(loc.latitude<0) line = line + tab + Math.abs(loc.latitude) + "-S";
			else line = line + tab + Math.abs(loc.latitude) + "-N";
			
			if(loc.longitude<0) line = line + tab + Math.abs(loc.longitude) + "-W";
			else line = line + tab + Math.abs(loc.longitude) + "-E";
			
			line = line + tab + loc.ListToString(loc.variant, ",") + tab + loc.ListToString(loc.metarea, "-") + tab + loc.ListToString(loc.good, ",")+ tab + loc.ListToString(loc.bad, ",");
			write.add(line);
		}
		
		TFile.write(new File("locations/uk.txt"), write);
	}
	
	/**
	 * Returns location in the form of a list
	 * @return
	 */
	private ArrayList<location> getList()
	{
		ArrayList<location> locs = new ArrayList<location>();
		location loc;
		
		Iterator iter = list.entrySet().iterator();
	    while (iter.hasNext()) {
	        Map.Entry<String,location> val = (Map.Entry<String,location>)iter.next();
	        loc = val.getValue();
	        locs.add(loc);
	    }
	    
	    return locs;
	}
	
	/**
	 * Gets hashmap of terms related to given location
	 * 1 - location or variant name
	 * 2 - good words
	 * 3 - bad words
	 * @param loc
	 * @return
	 */
	public HashMap<String,Integer> getHash(String loc)
	{
		HashMap<String,Integer> set = new HashMap<String,Integer>();
		set.put(loc,1);
		location l = list.get(loc);
		
		
		// If location not in locList then simply return map with 1 entry
		if(!list.containsKey(loc)) return set;
		
		//Get variant names
		for(int i=0;i<l.variant.size();i++) set.put(l.variant.get(i),1);
		
		//Get good words
		for(int i=0;i<l.good.size();i++) set.put(l.good.get(i),2);
		
		//Get bad words
		for(int i=0;i<l.bad.size();i++) set.put(l.bad.get(i),3);
		
		return set;
	}
	
	/**
	 * Iterates through locations and inserts to good array, locs which are in its metropolitan area
	 */
	private void addGoodMetarea()
	{
		ArrayList<location> locs = getList();
		location loc, met;
		
		for(int i=0;i<locs.size();i++)
		{
			loc = locs.get(i);
			
			for(int j=0;j<loc.metarea.size();j++)
			{
				if(list.containsKey(loc.metarea.get(j)))
				{
					met = list.get(loc.metarea.get(j));
					if(!met.name.equals(loc.name)) met.good.add(loc.name);
					if(debug==1) System.out.println("ADD "+loc.name+" TO GOOD "+met.name);
				}
			}
		}
	}
	
	/**
	 * Add to good list for location loc
	 * @param loc
	 * @param good
	 */
	public void addToGood(String loc, String good)
	{
		location l = list.get(loc);
		
		l.good.add(good);
		
		list.put(l.name, l);
	}
	
	/**
	 * Add to bad list for location loc
	 * @param loc
	 * @param bad
	 */
	public void addToBad(String loc, String bad)
	{
		location l = list.get(loc);
		
		l.bad.add(bad);
		
		list.put(l.name, l);
	}
	
	public static void main(String[] args)
	{
		locList uk = new locList();
		uk.load("locations/uk.txt");
		//uk.loadGrid();
		ArrayList<location> test = uk.getNeighbours(args[0], Double.parseDouble(args[1]));
		for(int i=0;i<test.size();i++) System.out.println(test.get(i).name + " : "+test.get(i).distance);
		
		/*double geo = 0.2;
		ArrayList<String> list = new ArrayList<String>();
		
		while(!list.contains("cardiff"))
		{
			ArrayList<String> n = uk.getNeighbours("bristol", geo);
			for(int i=0;i<n.size();i++) list.add(n.get(i));
			
			System.out.println("DOING "+geo);
			geo = geo + 0.1;
			
			if(geo > 10.0) break;
		}
		System.out.println("TOTAL IS" + uk.total);*/
		//uk.print();
		//for(int i=0;i<list.size();i++) System.out.println(list.get(i));
		//uk.addGoodMetArea();
		//uk.toFile();
	}
}
